package com.pureland.storage;

import com.pureland.storage.bean.Storage;
import com.pureland.storage.bean.StorageResult;
import com.pureland.storage.properties.StorageProperties;
import com.pureland.storage.utils.FileUtils;
import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @projectName: pureland_cloud
 * @className: com.pureland.storage.MinIOStorageTemplate
 * @description: MinIO对象存储实现
 * @author: tong.li
 * @createTime: 2020/10/19 15:13
 * @version: v1.0
 * @copyright: 版权所有 李彤 © 2020
 */
@Slf4j
@RequiredArgsConstructor
public class MinIOStorageTemplate implements StorageTemplate {

    /**
     * MinIO配置属性类
     */
    private final StorageProperties.MinIO storageProperties;

    /**
     * MinIO客户端
     */
    private volatile MinioClient minioClient;

    @Override
    @PostConstruct
    public void init() {
        // 生成Minio客户端
        minioClient = MinioClient.builder()
                // 设置MinIO的对象存储服务地址,即使URL
                .endpoint(storageProperties.getEndpoint(),storageProperties.getPort(),storageProperties.isSecure())
                // 设置化用户身份信息(accessKey和secretKey)
                .credentials(storageProperties.getAccessKey(),storageProperties.getSecretKey())
                // 设置区域
                .region(storageProperties.getRegion())
                // 构建MinioClient
                .build();
    }


    @Override
    public StorageResult upload(Storage storage, String url, String contentType) {
        storage = Storage.checkDefaultStorage(storage,null,this.storageProperties.getBucket());
        String bucketName = storage.getBucketName();
        bucketName = StringUtils.isEmpty(bucketName) ? storageProperties.getBucket() : bucketName;
        final String originName = storage.getOriginName();
        String targetName = FileUtils.targetName(storage.isRandomName(), getPrefix(storage.getRule(), storage.getPrefix()), originName);
        // MinIO对象名称不支持以/斜线开始，具体原因请参照:https://github.com/minio/minio/issues/7717和https://github.com/minio/minio/issues/5978
        if (!StringUtils.isEmpty(targetName) && targetName.startsWith(FileUtils.SEPARATOR)) {
            targetName = targetName.substring(1);
        }
        InputStream inputStream = null;
        try {
            FileUtils.FileContentInfo remoteFileContentInfo = FileUtils.getRemoteFileContentInfo(url, true);
            inputStream = remoteFileContentInfo.getInputStream();
            // 设置扩展名
            if (!StringUtils.isEmpty(targetName)) {
                targetName = targetName + remoteFileContentInfo.getExtension();
            }
            // 获取ContenType
            if (StringUtils.isEmpty(contentType)) {
                contentType = remoteFileContentInfo.getMimeTypeName();
            }
            // 从输入流上传
            long contentLength = remoteFileContentInfo.getContentSize();
            // 执行上传操作
            execUpload(inputStream, remoteFileContentInfo.getMimeTypeName(), contentLength, bucketName, targetName);
            String relativeUrl = "/" + bucketName + "/" + targetName;
            return StorageResult.builder()
                    .originName(originName)
                    .targetName(targetName)
                    // 绝对路径,方便前端直接获取
                    .absoluteUrl(this.storageProperties.getEndpoint() + ":" + this.storageProperties.getPort() + relativeUrl)
                    // 后端数据库存储建议较短的相对路径
                    .relativeUrl(relativeUrl)
                    .build();
        } catch (Exception e) {
            log.error("MinIO Remote putObject for url:{} error",url,e);
        } finally {
            // 上传完毕后关闭流
            try {
                IOUtils.close(inputStream);
            } catch (IOException e) {
            }
        }
        return null;
    }

    @Override
    public StorageResult upload(Storage storage, File file, String contentType) {
        if (Objects.isNull(file) || !file.exists() || file.isDirectory()) {
            throw new RuntimeException("file check fail");
        }
        // 设置原始的文件名称
        storage.setOriginName(file.getName());
        storage = Storage.checkDefaultStorage(storage, file.getName(),this.storageProperties.getBucket());
        String bucketName = storage.getBucketName();
        bucketName = StringUtils.isEmpty(bucketName) ? storageProperties.getBucket() : bucketName;
        final String originName = storage.getOriginName();
        final String prefix = getPrefix(storage.getRule(), storage.getPrefix());
        String targetName = FileUtils.targetName(storage.isRandomName(), prefix, originName);
        // MinIO对象名称不支持以/斜线开始，具体原因请参照:https://github.com/minio/minio/issues/7717
        if (!StringUtils.isEmpty(targetName) && targetName.startsWith(FileUtils.SEPARATOR)) {
            targetName = targetName.substring(1);
        }
        FileUtils.FileContentInfo localFileContentInfo = FileUtils.getLocalFileContentInfo(file, true);
        InputStream inputStream = localFileContentInfo.getInputStream();
        try {
            // 获取去文件/流大小
            long contentLength = localFileContentInfo.getContentSize();
            // 获取ContenType
            if (StringUtils.isEmpty(contentType)) {
                 contentType = localFileContentInfo.getMimeTypeName();
            }
            // 构建上传参数
            execUpload(inputStream, contentType, contentLength, bucketName, targetName);
            String relativeUrl = "/" + bucketName + "/" + targetName;
            return StorageResult.builder()
                    .originName(originName)
                    .targetName(targetName)
                    // 绝对路径,方便前端直接获取
                    .absoluteUrl(this.storageProperties.getEndpoint() + ":" + this.storageProperties.getPort() + relativeUrl)
                    // 后端数据库存储建议较短的相对路径
                    .relativeUrl(relativeUrl)
                    .build();
        } catch (Exception e) {
            log.error("MinIO Local putObject for url:{} error",file.getAbsolutePath(),e);
        }finally {
            // 上传完毕后关闭流
            try {
                IOUtils.close(inputStream);
            } catch (IOException e) {
                log.error("upload IOUtils.close(bis) error", e);
            }
        }
        return null;
    }



    @Override
    public StorageResult upload(Storage storage,  InputStream inputStream, String contentType,  long contentLength) {
        if (Objects.isNull(inputStream)) {
            throw new RuntimeException("inputStream is not null");
        }
        storage = Storage.checkDefaultStorage(storage, null, this.storageProperties.getBucket());
        String bucketName = storage.getBucketName();
        bucketName = StringUtils.isEmpty(bucketName) ? storageProperties.getBucket() : bucketName;
        final String prefix = getPrefix(storage.getRule(), storage.getPrefix());
        String targetName = FileUtils.targetName(storage.isRandomName(), prefix, storage.getOriginName());
        // MinIO对象名称不支持以/斜线开始，具体原因请参照:https://github.com/minio/minio/issues/7717
        if (!StringUtils.isEmpty(targetName) && targetName.startsWith(FileUtils.SEPARATOR)) {
            targetName = targetName.substring(1);
        }
        FileUtils.FileContentInfo inputStreamFileContentInfo = null;
        inputStreamFileContentInfo = FileUtils.getInputStreamFileContentInfo(inputStream);
        inputStream = inputStreamFileContentInfo.getInputStream();
        try {
            // 获取去文件/流大小
            contentLength = (contentLength == FileUtils.DEFAULT_CONTENT_LENGTH_UNKOWN ? inputStreamFileContentInfo.getContentSize() : contentLength);
            // 获取ContenType
            if (StringUtils.isEmpty(contentType)) {
                contentType = inputStreamFileContentInfo.getMimeTypeName();
            }
            // 构建上传参数
            execUpload(inputStream, contentType, contentLength, bucketName, targetName);
            String relativeUrl = "/" + bucketName + "/" + targetName;
            return StorageResult.builder()
                    .originName(targetName)
                    .targetName(targetName)
                     // 绝对路径,方便前端直接获取
                    .absoluteUrl(this.storageProperties.getEndpoint() + ":" + this.storageProperties.getPort() + relativeUrl)
                    // 后端数据库存储建议较短的相对路径
                    .relativeUrl(relativeUrl)
                    .build();
        } catch (Exception e) {
            log.error("MinIO InputStream putObject for inputStream error",e);
        }finally {
            // 上传完毕后关闭流
            try {
                IOUtils.close(inputStream);
            } catch (IOException e) {
                log.error("upload IOUtils.close(bis) error", e);
            }
        }
        return null;
    }

    /**
     * 执行上传操作
     * @param inputStream
     * @param contentType
     * @param contentLength
     * @param bucketName
     * @param targetName
     * @return
     * @throws IOException
     * @throws InvalidKeyException
     * @throws InvalidResponseException
     * @throws RegionConflictException
     * @throws InsufficientDataException
     * @throws NoSuchAlgorithmException
     * @throws ServerException
     * @throws InternalException
     * @throws XmlParserException
     * @throws InvalidBucketNameException
     * @throws ErrorResponseException
     */
    private ObjectWriteResponse execUpload(InputStream inputStream, String contentType, long contentLength, String bucketName, String targetName) throws IOException, InvalidKeyException, InvalidResponseException, RegionConflictException, InsufficientDataException, NoSuchAlgorithmException, ServerException, InternalException, XmlParserException, InvalidBucketNameException, ErrorResponseException {
        // 构建上传参数
        PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                .bucket(bucketName)
                .object(targetName)
                .contentType(contentType)
                .stream(inputStream, contentLength, FileUtils.DEFAULT_CONTENT_LENGTH_UNKOWN)
                .build();
        // 上传之前校验Bucket
        checkBucketAndCreate(minioClient, bucketName);
        return minioClient.putObject(putObjectArgs);
    }


    @Override
    public StorageResult upload(Storage storage, byte[] bytes) {
        if (Objects.isNull(bytes) && bytes.length <= 0) {
            throw new RuntimeException("bytes is not null and bytes.length > 0 ");
        }
        InputStream is = null;
        StorageResult upload = null;
        try {
            // 将字节转化为输入流
            is =  new ByteArrayInputStream(bytes);
            upload = this.upload(storage, is, FileUtils.DEFAULT_CONTENT_TYPE, FileUtils.DEFAULT_CONTENT_LENGTH_UNKOWN);
        } catch (Exception e) {
            log.error("MinIO byte[] putObject for inputStream error",e);
        }finally {
            // 上传完毕后关闭流
            try {
                IOUtils.close(is);
            } catch (IOException e) {
                log.error("upload byte[] IOUtils.close(is) error", e);
            }
        }
        return upload;
    }



    @Override
    public void checkBucketAndCreate(MinioClient minioClient, String bucketName) throws IOException, InvalidKeyException, InvalidResponseException, RegionConflictException, InsufficientDataException, NoSuchAlgorithmException, ServerException, InternalException, XmlParserException, InvalidBucketNameException, ErrorResponseException {
        if (StringUtils.isEmpty(bucketName)) {
            new IllegalArgumentException("Minio bucket is not null!");
        }
        BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build();
        boolean isExist = minioClient.bucketExists(bucketExistsArgs);
        if(!isExist){
            // 创建存储桶
            // 这里注意文件系统模式不支持对象锁定objectLock(true)或版本控制,详情请参考:https://github.com/minio/minio/issues/10593
            MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder()
                    .bucket(bucketName)
                    .build();
            minioClient.makeBucket(makeBucketArgs);
            // 注意这里只是创建了Bucket,没有设置权限(Bucket Policy)，因为代码设置Bucket权限有点麻烦，所以我们可以在web管理界面设置
            // minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucketName).build());
        }
    }


    @Override
    public InputStream getObjectStream(String bucketName, String objectName) {
        InputStream is = null;
        try {
            GetObjectArgs objectArg = GetObjectArgs.builder().bucket(bucketName).object(objectName).build();
            is = minioClient.getObject(objectArg);
        } catch (Exception e) {
            log.error("getObject for stream error",e);
            // 上传完毕后关闭流
            try {
                IOUtils.close(is);
            } catch (IOException ex) {
                log.error("getObject for stream IOUtils.close(bis) error", ex);
            }
        }
        return is;
    }



    @Override
    public String getPublicObjectUrl(String bucketName, String objectName) {
        checkBaseName(bucketName, objectName);
        String url = "";
        try {
            url = minioClient.getObjectUrl(bucketName, objectName);
        } catch (Exception e) {
            log.error("getPublicObjectUrl  error",e);
        }
        return url;
    }


    @Override
    public String getPresignedObjectUrl(String bucketName, String objectName) {
        checkBaseName(bucketName, objectName);
        String url = "";
        try {
            GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
                    .method(Method.GET)
                    // 设置有效期,默认1天后失效
                    .expiry(1, TimeUnit.DAYS)
                    .bucket(bucketName)
                    .object(objectName).build();
            url = minioClient.getPresignedObjectUrl(args);
        } catch (Exception e) {
            log.error("getPresignedObjectUrl for stream error",e);
        }
        return url;
    }

    /**
     * 校验最基本的名称(bucketName和objectName)
     * @param bucketName
     * @param objectName
     * @throws IllegalArgumentException
     */
    private void checkBaseName(String bucketName, String objectName) throws IllegalArgumentException {
        if (StringUtils.isEmpty(bucketName) || StringUtils.isEmpty(objectName)) {
            throw new IllegalArgumentException("bucketName or objectName is not null");
        }
    }


}
